package mimir

// todo test utf-8 in subject, sender (mímir), body

// ---- release v1

/* todo views:
default: threads click-> thread
thread: linear messages by datetime oldest on top, with #message_id, and is_reply_to: <a href=#message_id>

search ordered by datetime, newest on top; thread – most recent message

search by time: messages [in thread] click-> thread#message_id
search by from: messages [in thread] click-> thread#message_id
filter by category: inherit
|search by subject: threads click-> thread
|search by subject+: messages [in thread] click-> thread#message_id
|search by full text: messages [in thread] click-> thread#message_id
*/

/* todo moderation:
ban address, right to forget, unsubscribe (from one topic, from all topics)
*/

// ---- release v2

// todo in thread card: add number of messages and interested people
// todo highlight patches
// todo check pgp/mime signatures

import (
	"bytes"
	"database/sql"
	"embed"
	"errors"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"regexp"
	"strconv"
	"strings"

	"apiote.xyz/p/asgard/himinbjorg"
	"apiote.xyz/p/asgard/idavollr"
	"apiote.xyz/p/asgard/jotunheim"

	"apiote.xyz/p/gott/v2"
	"github.com/emersion/go-imap"
	_ "github.com/emersion/go-message/charset"
	"github.com/emersion/go-msgauth/dkim"
)

type UnknownCategoryError struct {
	MessageID string
	Category  string
}

func (e UnknownCategoryError) Error() string {
	return fmt.Sprintf("Unknown category ‘%s’ in message %s", e.Category, e.MessageID)
}

type ListingPage struct {
	Messages []himinbjorg.Message
	Page     int
	NumPages int
}

func (l ListingPage) PrevPage() int {
	return l.Page - 1
}
func (l ListingPage) NextPage() int {
	return l.Page + 1
}

type MimirMailbox struct {
	idavollr.Mailbox
	categories     []string
	categoryRegexp *regexp.Regexp
	db             *sql.DB
}

type MimirImapMessage struct {
	idavollr.ImapMessage
	categoryRegexp *regexp.Regexp
	categories     []string
	category       string
	dkimStatus     bool
	db             *sql.DB
	config         jotunheim.Config
	recipients     []string
	mboxName       string
}

func Mimir(db *sql.DB, config jotunheim.Config) error {
	mailbox := &MimirMailbox{
		Mailbox: idavollr.Mailbox{
			MboxName: config.Mimir.ImapInbox,
			ImapAdr:  config.Mimir.ImapAddress,
			ImapUser: config.Mimir.ImapUsername,
			ImapPass: config.Mimir.ImapPassword,
			Conf:     config,
		},
		db: db,
	}

	mailbox.SetupChannels()
	r := gott.R[idavollr.AbstractMailbox]{
		S: mailbox,
		LogLevel: gott.Info,
	}.
		Bind(getCategories).
		Bind(prepareCategoryRegexp).
		Bind(idavollr.Connect).
		Tee(idavollr.Login).
		Bind(idavollr.SelectInbox).
		Tee(idavollr.CheckEmptyBox).
		Map(idavollr.FetchMessages).
		Tee(archiveMessages).
		Tee(idavollr.CheckFetchError).
		Tee(idavollr.Expunge).
		Recover(idavollr.IgnoreEmptyBox).
		Recover(idavollr.Disconnect)

	return r.E
}

func getCategories(am idavollr.AbstractMailbox) (idavollr.AbstractMailbox, error) {
	m := am.(*MimirMailbox)
	m.categories = m.Config().Mimir.Categories
	if len(m.categories) == 0 {
		return m, errors.New("no categories defined")
	}
	return m, nil
}

func prepareCategoryRegexp(am idavollr.AbstractMailbox) (idavollr.AbstractMailbox, error) {
	m := am.(*MimirMailbox)
	if !strings.Contains(m.Config().Mimir.RecipientTemplate, "[:]") {
		return m, errors.New("recipient template does not contain ‘[:]’")
	}
	recipientRegexp := strings.Replace(m.Config().Mimir.RecipientTemplate, "[:]", "(.*)", 1)
	r, err := regexp.Compile(recipientRegexp)
	m.categoryRegexp = r
	return m, err
}

func archiveMessages(am idavollr.AbstractMailbox) error {
	m := am.(*MimirMailbox)
	for msg := range m.Messages() {
		imapMessage := idavollr.ImapMessage{
			Msg:      msg,
			Sect:     m.Section(),
			Mimetype: "text/plain",
		}
		imapMessage.SetClient(m.Client())
		r := gott.R[idavollr.AbstractImapMessage]{
			S: &MimirImapMessage{
				ImapMessage:    imapMessage,
				categoryRegexp: m.categoryRegexp,
				categories:     m.categories,
				db:             m.db,
				config:         m.Config(),
				mboxName:       m.MboxName,
			},
		}.
			Bind(getMessageCategory).
			Bind(idavollr.ReadMessageBody).
			Bind(verifyDkim).
			Bind(idavollr.ParseMimeMessage).
			Bind(idavollr.GetBody).
			Bind(idavollr.ReadBody).
			Tee(archiveMessage).
			Tee(updateTopicRecipients).
			Bind(getMessageRecipients).
			Tee(forwardMimirMessage).
			Recover(idavollr.RecoverMalformedMessage).
			Recover(recoverUnknownCategory).
			Tee(removeMessage).
			Recover(idavollr.RecoverErroredMessages)
		if r.E != nil {
			return r.E
		}
	}
	return nil
}

func getMessageCategory(am idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
	m := am.(*MimirImapMessage)
	categoryInAddress := ""
	recipients := append(m.Message().Envelope.To, m.Message().Envelope.Cc...)
	for _, recipient := range recipients {
		matches := m.categoryRegexp.FindStringSubmatch(recipient.Address())
		if len(matches) != 2 {
			continue
		}
		categoryInAddress = matches[1]
		for _, category := range m.categories {
			if matches[1] == category {
				m.category = category
				return m, nil
			}
		}
	}
	return m, UnknownCategoryError{
		MessageID: m.Message().Envelope.MessageId,
		Category:  categoryInAddress,
	}
}

func verifyDkim(am idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
	m := am.(*MimirImapMessage)
	dkimStatus := false
	r := bytes.NewReader(m.MessageBytes())
	verifications, err := dkim.Verify(r)
	if err != nil {
		return m, err
	}
	for _, v := range verifications {
		if v.Err == nil && m.Message().Envelope.From[0].HostName == v.Domain {
			dkimStatus = true
		}
	}
	m.dkimStatus = dkimStatus
	return m, nil
}

func archiveMessage(am idavollr.AbstractImapMessage) error {
	m := am.(*MimirImapMessage)
	messageID := m.Message().Envelope.MessageId
	subject := m.Message().Envelope.Subject
	date := m.Message().Envelope.Date.UTC()
	inReplyTo := m.Message().Envelope.InReplyTo
	sender := m.Message().Envelope.From[0]
	log.Printf("archiving %s\n", messageID)
	return himinbjorg.AddArchiveEntry(m.db, messageID, m.category, subject, m.MessageBody(), date, inReplyTo, m.dkimStatus, sender, string(m.MessageBytes()))
}

func updateTopicRecipients(am idavollr.AbstractImapMessage) error {
	m := am.(*MimirImapMessage)
	var sender *imap.Address
	if len(m.Message().Envelope.ReplyTo) > 0 {
		sender = m.Message().Envelope.ReplyTo[0]
	} else {
		sender = m.Message().Envelope.From[0]
	}
	messageID := m.Message().Envelope.MessageId
	return himinbjorg.UpdateRecipients(m.db, sender, messageID)
}

func getMessageRecipients(am idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
	m := am.(*MimirImapMessage)
	sender := m.Message().Envelope.From[0]
	messageID := m.Message().Envelope.MessageId
	recipients, err := himinbjorg.GetRecipients(m.db, messageID, sender)
	if sender.Address() != m.config.Mimir.PersonalAddress {
		recipients = append(recipients, strings.Replace(m.config.Mimir.ForwardAddress, "[:]", m.category, 1))
	}
	m.recipients = recipients
	return m, err
}

func forwardMimirMessage(am idavollr.AbstractImapMessage) error {
	m := am.(*MimirImapMessage)
	messageID := m.Message().Envelope.MessageId
	inReplyTo := m.Message().Envelope.InReplyTo
	subject := m.Message().Envelope.Subject
	sender := m.Message().Envelope.From[0]
	log.Printf("forwarding %s to %v\n", messageID, m.recipients)
	return idavollr.ForwardMessage(m.config, m.category, messageID, inReplyTo, subject, m.MessageBody(), m.recipients, sender)
}

func recoverUnknownCategory(m idavollr.AbstractImapMessage, err error) (idavollr.AbstractImapMessage, error) {
	var unknownCategoryError UnknownCategoryError
	if errors.As(err, &unknownCategoryError) {
		err = nil
		log.Println(unknownCategoryError.Error())
	}
	return m, err
}

func removeMessage(am idavollr.AbstractImapMessage) error {
	m := am.(*MimirImapMessage)
	return idavollr.RemoveMessage(m.Client(), m.Message().Uid, m.mboxName)
}

func Serve(db *sql.DB, templatesFs embed.FS) func(w http.ResponseWriter, r *http.Request) {
	// TODO on back check with cache
	return func(w http.ResponseWriter, r *http.Request) {
		path := strings.Split(r.URL.Path[1:], "/")
		if len(path) == 1 {
			r.ParseForm()
			pageParam := r.Form.Get("page")
			var (
				page int64 = 1
				err  error
			)
			if pageParam != "" {
				page, err = strconv.ParseInt(pageParam, 10, 0)
				if err != nil {
					w.WriteHeader(http.StatusInternalServerError)
					log.Println(err)
				}
			}
			messages, numThreads, err := himinbjorg.GetArchivedThreads(db, page)
			if err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				log.Println(err)
			}
			t, err := template.ParseFS(templatesFs, "templates/mimir_threads.html")
			if err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				log.Println(err)
			}
			b := bytes.NewBuffer([]byte{})
			err = t.Execute(b, ListingPage{
				Messages: messages,
				Page:     int(page),
				NumPages: numThreads / 12,
			})
			w.Write(b.Bytes())
		} else if len(path) == 3 && path[1] == "m" {
			thread, err := himinbjorg.GetArchivedThread(db, path[2])
			if err != nil {
				var noMsgErr himinbjorg.NoMessageError
				if errors.As(err, &noMsgErr) {
					w.WriteHeader(http.StatusNotFound)
					w.Write([]byte(noMsgErr.Error()))
				} else {
					w.WriteHeader(http.StatusInternalServerError)
					log.Println(err)
				}
				return
			}
			t, err := template.ParseFS(templatesFs, "templates/mimir_message.html")
			if err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				log.Println(err)
			}
			b := bytes.NewBuffer([]byte{})
			err = t.Execute(b, thread)
			w.Write(b.Bytes())
		} else {
			w.WriteHeader(http.StatusNotFound)
		}
	}
}
